﻿using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq.Expressions;
using System.Windows.Media;
using System.Xml.Serialization;

namespace be.metanous.mvvm.core
{
    public class ViewModelBase : INotifyPropertyChanged, ISuspendNotifications, IDisposable, IDataErrorInfo, IRequestPropertyValidation, INotifyPropertyChanging
    {
        #region StandardProperties

        /// <summary>
        /// Returns the user-friendly name of this viewmodel.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        private string _name;
        [XmlIgnore]
        public virtual string Name
        {
            get { return _name; }
            set
            {
                if (value != _name)
                {
                    _name = value;

                    RaisePropertyChanged(()=>Name);
                }
            }
        }

        /// <summary>
        /// Returns the selected state of this viewmodel.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        ///
        private bool _isSelected;
        [XmlIgnore]
        public virtual bool IsSelected
        {
            get { return _isSelected; }
            set
            {
                if (_isSelected != value)
                {
                    _isSelected = value;

                    RaisePropertyChanged(() => IsSelected);
                }
            }
        }

        /// <summary>
        /// Returns the enabled state of this viewmodel.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        ///
        private bool _isEnabled = true;
        [XmlIgnore]
        public virtual bool IsEnabled
        {
            get { return _isEnabled; }
            set
            {
                if (_isEnabled != value)
                {
                    _isEnabled = value;
                    RaisePropertyChanged(() => IsEnabled);
                }
            }
        }

        /// <summary>
        /// Returns the visible state of this viewmodel.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        private bool _isVisible;
        [XmlIgnore]
        public virtual bool IsVisible
        {
            get { return _isVisible; }
            set
            {
                if (_isVisible != value)
                {
                    _isVisible = value;
                    RaisePropertyChanged(() => IsVisible);
                }
            }
        }

        /// <summary>
        /// Returns the focus state of this viewmodel.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        private bool _isFocused;
        [XmlIgnore]
        public bool IsFocused
        {
            get { return _isFocused; }
            set
            {
                if (_isFocused != value)
                {
                    _isFocused = value;
                    RaisePropertyChanged(() => IsFocused);
                }
            }
        }

        /// <summary>
        /// Returns the tooltip of this viewmodel.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        private string _toolTip;
        [XmlIgnore]
        public virtual string ToolTip
        {
            get { return _toolTip; }
            set
            {
                if (_toolTip != value)
                {
                    _toolTip = value;
                    RaisePropertyChanged(() => ToolTip);
                }
            }
        }

     
        /// <summary>
        /// Returns the image source of this viewmodel dirived from the given image.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        private ImageSource _imageSource;
        [XmlIgnore]
        public virtual ImageSource ImageSource
        {
            get { return _imageSource; }
            set
            {
                if (_imageSource != value)
                {
                    _imageSource = value;

                    RaisePropertyChanged(() => ImageSource);
                }
            }
        }

        /// <summary>
        /// Returns the contained object of this viewmodel.
        /// Child classes can set this property to a new value,
        /// or override it to determine the value on-demand.
        /// </summary>
        private object _containedObject;
        [XmlIgnore]
        public object ContainedObject
        {
            get { return _containedObject; }
            set
            {
                if (_containedObject != value)
                {
                    _containedObject = value;

                    RaisePropertyChanged(() => ContainedObject);
                }
            }
        }

        public override string ToString()
        {
            if (Name != null) return Name;
            return base.ToString();
        }

        #endregion // StandardProperties

        #region Debugging Aides

        /// <summary>
        /// Warns the developer if this object does not have
        /// a public property with the specified name. This 
        /// method does not exist in a Release build.
        /// </summary>
        [Conditional("DEBUG")]
        [DebuggerStepThrough]
        public void VerifyPropertyName(string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                return;

            // Verify that the property name matches a real,  
            // public, instance property on this object.
            if (GetType().GetProperty(propertyName) == null)
            {
                string msg = "Invalid property name: " + propertyName;

                if (ThrowOnInvalidPropertyName)
                    throw new Exception(msg);

                Debug.Fail(msg);
            }
        }

        /// <summary>
        /// Returns whether an exception is thrown, or if a Debug.Fail() is used
        /// when an invalid property name is passed to the VerifyPropertyName method.
        /// The default value is false, but subclasses used by unit tests might 
        /// override this property's getter to return true.
        /// </summary>
        protected virtual bool ThrowOnInvalidPropertyName { get; private set; }

        #endregion // Debugging Aides

        #region Command

        public Command CreateCommand(Action<object> execute)
        {
            return new Command(execute);
        }

        public Command CreateCommand(Action<object> execute, Predicate<object> canExecute)
        {
            return new Command(execute, canExecute);
        }

        #endregion // Command

        #region ActionDispatcher

        /// <summary>
        /// Invokes the specified action on the UI thread.
        /// </summary>
        /// <param name="action">An Action to be invoked on the UI thread.</param>
        public static void InvokeOnUiThread(Action action)
        {
            var dispatcher = System.Windows.Threading.Dispatcher.CurrentDispatcher;
            if (dispatcher.CheckAccess())
            {
                action();
            }
            else
            {
                dispatcher.BeginInvoke(action);
            }
        }

        #endregion // ActionDispatcher

        #region Static Reflection

        public static string PropertyName<T>(Expression<Func<T>> expression)
        {
            if (expression.Body is UnaryExpression)
            {
                var unaryExpression = expression.Body as UnaryExpression;

                if (unaryExpression == null)
                {
                    return null;
                }

                var memberExpression = unaryExpression.Operand as MemberExpression;

                if (memberExpression == null)
                {
                    return null;
                }

                return memberExpression.Member.Name;
            }
            if (expression.Body is MemberExpression)
            {
                var memberExpression = expression.Body as MemberExpression;

                if (memberExpression == null)
                {
                    return null;
                }

                return memberExpression.Member.Name;
            }
            return null;
        }

        #endregion // Static Reflection

        #region INotifyPropertyChanged Members

        /// <summary>
        /// Raised when a property on this object has a new value.
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;

        /// <summary>
        /// Raises this object's PropertyChanged event for all properties.
        /// </summary>
        public virtual void RaisePropertyChanged()
        {
            RaisePropertyChanged(null as String, null, null);
        }

        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="expression">The property name expression that has a new value.</param>
        /// <param name="oldValue">the old value</param>
        /// <param name="newValue">the new value</param>
        public virtual void RaisePropertyChanged(Expression<Func<object>> expression, object oldValue = null, object newValue = null)
        {
            RaisePropertyChanged(PropertyName(expression), oldValue, newValue);
        }

        /// <summary>
        /// Raises this object's PropertyChanged event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        /// <param name="oldValue">the old value</param>
        /// <param name="newValue">the new value</param>
        public virtual void RaisePropertyChanged(string propertyName, object oldValue = null, object newValue = null)
        {
            if (!NotificationsSuspended)
            {
                VerifyPropertyName(propertyName);

                PropertyChangedEventHandler handler = PropertyChanged;
                if (handler != null)
                {
                    var e = new PropertyNotificationEventArgs(propertyName, oldValue, newValue);
                    handler(this, e);
                }
            }
            else
            {
                IsDirty = true;
            }
        }

        #endregion // INotifyPropertyChanged Members

        #region ISuspendNotifications

        private bool _notificationsSuspended;

        /// <summary>
        /// Gets a value indicating whether change notifications are suspended.
        /// </summary>
        /// <value>
        /// 	<c>True</c> if notifications are suspended, otherwise, <c>false</c>.
        /// </value>
        public bool NotificationsSuspended
        {
            get { return _notificationsSuspended; }
        }

        /// <summary>
        /// Gets or sets a value indicating whether change to the viewmodel is made when
        /// its notifications are suspended.
        /// </summary>
        /// <value><c>true</c> if this instance is has been changed while notifications are
        /// suspended; otherwise, <c>false</c>.</value>
        protected bool IsDirty { get; set; }

        public virtual void SuspendNotifications()
        {
            _notificationsSuspended = true;
        }

        public virtual void ResumeNotifications()
        {
            _notificationsSuspended = false;
            if (IsDirty)
            {
                RaisePropertyChanged();
            }
        }

        #endregion // ISuspendNotifications

        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing,
        /// or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Releases unmanaged and - optionally - managed resources
        /// </summary>
        /// <param name="disposing"><c>true</c> to release both managed and unmanaged resources;
        /// <c>false</c> to release only unmanaged resources.</param>
        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                // Dispose managed resources.
            }

            // There are no unmanaged resources to release, but
            // if we add them, they need to be released here.
        }

        /// <summary>
        /// Useful for ensuring that ViewModel objects are properly garbage collected.
        /// </summary>
        ~ViewModelBase()
        {
        }

        #endregion // IDisposable Members

        #region IDataErrorInfo

        public virtual string this[string propertyName]
        {
            get
            {
                var eventArgs = new PropertyValidationEventArgs(propertyName);
                RaisePropertyValidation(eventArgs);
                return eventArgs.Error;
            }
        }

        public virtual string Error
        {
            get { return String.Empty; }
        }

        #endregion IDataErrorInfo

        #region IRequestPropertyValidation

        public event PropertyValidationEventArgs.PropertyValidationEventHandler PropertyValidation;

        /// <summary>
        /// Raises this object's PropertyValidation event.
        /// </summary>
        /// <param name="eventArgs">The event arg with the given property name that needs validation</param>
        private void RaisePropertyValidation(PropertyValidationEventArgs eventArgs)
        {
            VerifyPropertyName(eventArgs.PropertyName);

            PropertyValidationEventArgs.PropertyValidationEventHandler handler = PropertyValidation;
            if (handler != null)
            {
                handler(this, eventArgs);
            }
        }

        #endregion IRequestPropertyValidation

        #region INotifyPropertyChanging Members

        /// <summary>
        /// Raised when a property on this object will change (issued before the change).
        /// </summary>
        public event PropertyChangingEventHandler PropertyChanging;


        /// <summary>
        /// Raises this object's PropertyChanging event.
        /// </summary>
        /// <param name="expression">The property name expression that has a new value.</param>
        protected virtual void RaisePropertyChanging(Expression<Func<object>> expression, object oldValue = null, object newValue = null)
        {
            RaisePropertyChanging(PropertyName(expression), oldValue, newValue);
        }

        /// <summary>
        /// Raises this object's PropertyChanging event.
        /// </summary>
        /// <param name="propertyName">The property that has a new value.</param>
        public virtual void RaisePropertyChanging(string propertyName, object oldValue = null, object newValue= null)
        {
            if (!NotificationsSuspended)
            {
                VerifyPropertyName(propertyName);

                PropertyChangingEventHandler handler = PropertyChanging;
                if (handler != null)
                {
                    var e = new PropertyChangingNotificationEventArgs(propertyName, oldValue, newValue);
                    handler(this, e);
                }
            }
        }

        #endregion // INotifyPropertyChanged Members   
    }
}
